#include <iostream>
#include <memory>
#include <sstream>

#ifdef _MSC_VER
#define _WINSOCKAPI_
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#include <iphlpapi.h>
#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x))
#define FREE(x) HeapFree(GetProcessHeap(), 0, (x))
#pragma comment(lib, "Iphlpapi.lib")
//Gavin: Necessary for M_PI to function properly
#define _USE_MATH_DEFINES // for C
#include <math.h>
#else
#include <ifaddrs.h>
#include <arpa/inet.h>
#endif

#include "spdlog/spdlog.h"

#include "Listener.hpp"
#include "AEyeSensorAPI.h"
#include "AEyeSDKEnumsToStr.hpp"
#include "SensorReader.hpp"
#include "PcapPacket.hpp"


namespace aeye {

namespace {
#ifdef _MSC_VER
std::string getLocalIP(std::string& netInterface) {
  std::string ipaddr = "";
  PIP_ADAPTER_INFO pAdapterInfo;
  PIP_ADAPTER_INFO pAdapter = NULL;
  DWORD dwRetVal = 0;
  UINT i;

  struct tm newtime;
  char buffer[32];
  errno_t error;

  ULONG ulOutBufLen = sizeof(IP_ADAPTER_INFO);
  pAdapterInfo = (IP_ADAPTER_INFO*)MALLOC(sizeof(IP_ADAPTER_INFO));
  if (pAdapterInfo == NULL) {
      std::cout << "Error allocating memory needed to call GetAdaptersinfo" << std::endl;
      exit(1);
  }

  if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) {
      FREE(pAdapterInfo);
      pAdapterInfo = (IP_ADAPTER_INFO*)MALLOC(ulOutBufLen);
      if (pAdapterInfo == NULL) {
          std::cout << "Error allocating memory needed to call GetAdaptersinfo" << std::endl;
          exit(1);
      }
  }

  if ((dwRetVal = GetAdaptersInfo(pAdapterInfo, &ulOutBufLen)) == NO_ERROR) {
      pAdapter = pAdapterInfo;
      while (pAdapter) {
          if ((std::string)pAdapter->Description == netInterface) {
              std::cout << "Found network interface:" << pAdapter->AdapterName << ", with IP:" << pAdapter->IpAddressList.IpAddress.String << std::endl;
              ipaddr = pAdapter->IpAddressList.IpAddress.String;
              break;
          }
          pAdapter = pAdapter->Next;
      }
  }
  else {
    printf("GetAdaptersInfo failed with error: %d\n", dwRetVal);
  }
  if (pAdapterInfo)
    FREE(pAdapterInfo);

  if (ipaddr.empty()) {
    std::stringstream ss;
    ss << "Network interface '" << netInterface << "' not available.";
    std::cout << ss.str() << " Assigning IP:127.0.0.1" << std::endl;
    ipaddr = "127.0.0.1";
    // Don't throw exception here! Otherwise we will not be able to edit this sensor in AEyeVis.
    //throw std::invalid_argument(ss.str());
  }

  return ipaddr;
}
#else
std::string getLocalIP(std::string& netInterface) {
  std::string local_ip;
  struct ifaddrs *if_addrs = nullptr;
  getifaddrs(&if_addrs);

  for (auto ifa = if_addrs; ifa != nullptr; ifa = ifa->ifa_next) {
    if (ifa->ifa_addr == nullptr) {
      continue;
    }
    // Check that interface is IPv4
    if (ifa->ifa_addr->sa_family != AF_INET) continue;

    void *address = &(reinterpret_cast<struct sockaddr_in*>(ifa->ifa_addr))->sin_addr;
    char address_buffer[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, address, address_buffer, INET_ADDRSTRLEN);
    std::string tmp_local_ip = address_buffer;

    if (netInterface.empty() && !tmp_local_ip.empty() && tmp_local_ip != "127.0.0.1") {
      // Just in case if caller don't know interface name and provided empty string as parameter and we found valid one.
      spdlog::info("Found network interface: {}, with IP: {}.", ifa->ifa_name, tmp_local_ip);
      local_ip = std::move(tmp_local_ip);
      netInterface = ifa->ifa_name;
      break;
    }
    else if (netInterface == ifa->ifa_name) {
      local_ip = std::move(tmp_local_ip);
    }
  }
  if (if_addrs != nullptr) {
    freeifaddrs(if_addrs);
  }
  if (local_ip.empty()) {
    std::stringstream ss;
    ss << "Network interface '" << netInterface << "' not available.";
    std::cout << ss.str() << " Assigning IP:127.0.0.1" << std::endl;
    local_ip = "127.0.0.1";
    // Don't throw exception here! Otherwise we will not be able to edit this sensor in AEyeVis.
    //throw std::invalid_argument(ss.str());
  }
  return local_ip;
}
#endif
}


Listener::Listener(const std::string& netInterface, const std::string& ipAddr, const std::string& sensorName,
               uint16_t cmdPort, uint16_t clientPointsPort, uint16_t clientImagePort, uint16_t serverPointsPort,
               uint16_t serverImgPort, uint16_t clientDiagPort)
               : Listener(SensorConfig{clientDiagPort},
                 DataPortConfig{clientPointsPort}, netInterface, ipAddr, sensorName) {
  (void) cmdPort; (void) serverImgPort; (void) serverPointsPort; (void) clientImagePort;
}

Listener::Listener(SensorConfig sensor_config, DataPortConfig data_ports,
                   std::string net_interface, std::string sensor_ip, std::string sensor_name)
: clientInterface(std::move(net_interface))
, sensorIp(std::move(sensor_ip))
, name(std::move(sensor_name))
, rangePort(data_ports.points)
, diagnosticPort(sensor_config.diagnostic_port)
, sensingOn(false)
, pcapModeOn(false) {
  spdlog::trace("Constructing Sensor with IP {} on {} named {}.", sensorIp, clientInterface, name);

  spdlog::trace("Selected local host IP: {}, for sensor: {} with IP: {}.", clientIp, name, sensorIp);

  clientIp = getLocalIP(clientInterface);
  sensorReader = std::make_unique<SensorReader>(sensorIp, rangePort, diagnosticPort,
                                                DEFAULT_CLIENT_HAWKEYE_PORT, name, true);

}
Listener::~Listener() {
  try { stop(); } catch (std::exception& e) {
    spdlog::warn("stop() raised exception: {}.", e.what());
  }
}

void Listener::setOnDiagnosticDataCallback(const std::function<void(DiagnosticData&&)>& processDiagnosticCallback) {
    sensorReader->setDiagnosticCallback(processDiagnosticCallback);
}

void Listener::setOnPointCloudFrameCallback(const std::function<void(PointCloudFrame&&)>& processPointCloudCallback) {
    sensorReader->setCloudCallback(processPointCloudCallback);
}


void Listener::start() {
    sensingOn = true;
    if (pcapModeOn) return;
    sensorReader->start();
}

void Listener::startDiagnostic() {
  sensorReader->startDiagnostic();
}

void Listener::disconnect() {
  try { stop(); } catch (std::exception& e) {
    spdlog::warn("stop() raised exception: {}.", e.what());
  }
}

void Listener::stop() {
    if (!sensingOn) return;

    sensingOn = false;
    sensorReader->stop();
}


const std::string& Listener::getInterface() const { return clientInterface; }
const std::string& Listener::getSensorIp()  const { return sensorIp;  }
const std::string& Listener::getName()      const { return name;      }
const std::string& Listener::getClientIp()  const { return clientIp;  }

uint16_t Listener::getRangePort()       const { return rangePort;       }
bool     Listener::isSensing()          const { return sensingOn;       }
bool     Listener::isPcapMode()         const { return pcapModeOn;      }

void Listener::processPcapPacket(PcapPacket& packet) {
    if (packet.sourceIpAddress() != sensorIp) { return; }
    if (!(pcapModeOn && sensingOn)) { return; }

    if (packet.sourcePort() == rangePort) {
        sensorReader->processRangingPcapPacket(packet);
    }
    else if (packet.sourcePort() == diagnosticPort) {
      sensorReader->processDiagnosticPcapPacket(packet);
    }
}

}